package org.diy.traffic.sources;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;

import org.diy.traffic.sinks.TwitterSink;
import org.diy.traffic.twitter.TrafficTwitter;
import org.diy.traffic.updates.TrafficUpdate;
import org.diy.traffic.util.TLog;

import winterwell.jtwitter.TwitterException;
import winterwell.jtwitter.TwitterRateLimitException;
import winterwell.jtwitter.Twitter.Message;
import winterwell.jtwitter.Twitter.Status;

/**
 * This class represents a Twitter Source which polls a Twitter account for
 * incoming Direct Messages, (presumably with a query representing a street
 * name) then searches through the account's tweets and returns any that match a
 * query string in the DM
 * 
 * @author Dan Greenblatt
 * 
 */
public class TwitterDMSource extends TwitterSource {

	/**
	 * This is a identifier unique to this TrafficSource subclass which should
	 * be prepended to any individual instance names when building a unique
	 * identifier string.
	 */
	private static String sourceType = "TWITTER_DM_SOURCE";

	/**
	 * a default value for update frequency (in seconds). Used if one isn't
	 * supplied in constructor.
	 */
	protected static final int DEFAULT_UPDATE_FREQUENCY_S = 30;

	/**
	 * Maintains a list of instances of TwitterDMSources. Can be used for client
	 * code to check whether a TwitterDMSource already exists for a given
	 * account.
	 */
	private static HashMap<String, TwitterDMSource> myInstances = new HashMap<String, TwitterDMSource>();

	/**
	 * A reference to a TrafficTweeter object which is used to handle
	 * communication with Twitter
	 */

	private long lastMsgId = -1;
	
	public static TwitterDMSource getInstanceByName(String name) {
		return myInstances.get(name);
	}

	public static TwitterDMSource createInstance(String username,
			String password) {
		return createInstance(username, password, DEFAULT_UPDATE_FREQUENCY_S);
	}

	public static TwitterDMSource createInstance(String username,
			String password, int updateFrequency) {
		TwitterDMSource tds = new TwitterDMSource(username, password,
				updateFrequency);
		myInstances.put(tds.getName(), tds);
		return tds;
	}

	/**
	 * Constructor can be used to specify credentials for Twitter account and an
	 * update frequency for this source.
	 * 
	 * @param username
	 * @param password
	 * @param updateFrequency
	 */
	public TwitterDMSource(String username, String password, int updateFrequency) {
		super(username, password, updateFrequency);
		this.sourceName = sourceType + "_" + username;
	}

	/**
	 * Constructor uses the default update frequency
	 * 
	 * @param username
	 * @param password
	 */
	public TwitterDMSource(String username, String password) {
		// check every thirty seconds....
		this(username, password, DEFAULT_UPDATE_FREQUENCY_S);
	}

	/**
	 * Performs the bulk of the work for checking this Source's Twitter account
	 * for Direct Messages
	 * 
	 * @updateLastCheck true if this should update the last check time to now
	 *                  after check is complete
	 * @return a list of TrafficUpdate objects; may be empty if there are new
	 *         new updates
	 */
	@Override
	public List<TrafficUpdate> doCheck(boolean updateLastCheck)
			throws TwitterDMSourceException {
		List<TrafficUpdate> updateList = new ArrayList<TrafficUpdate>();

		// if you've been told to chill out because you ran up against the
		// Twitter rate limit
		if (this.inChillaxMode()) {
			// update the last check, even though you didn't really check. this
			// will prevent the InputDaemon
			// from testing you every ten seconds...
			if (updateLastCheck)
				setLastCheckToNow();
			return null;
		}

		// if it's been more than 1800000milliseconds (==30minutes) since the
		// last time you've checked
		// to see if there are any new followers you need to follow, then do
		// this now.
		if ((System.currentTimeMillis() - myTwitter.getLastFollowerSynch()) > 1800000) {
			try {
				// try to follow all the people who follow you. this is REQUIRED
				// to be able to send
				// direct messages to them.
				TLog.log(Level.INFO, String.format(
						"[%s] attempting to follow all unfollowed followers",
						this.sourceName));
				myTwitter.followFollowers();
			} catch (TwitterException te) {
				if (te.getClass() == TwitterRateLimitException.class) {
					// wait 15 minutes before trying again ....
					long chillaxUntil = System.currentTimeMillis() + 600000;
					TLog.log(Level.INFO, String.format(
							"[%s] laying low until %s", this.sourceName,
							new Date(chillaxUntil)));
					setChillaxUntil(chillaxUntil);
					throw new TwitterDMSourceException(
							"Error occurred while following followers: "
									+ te.getMessage());
				} else
					TLog.log(Level.WARNING,
							"Unkown error occurred while following followers: "
									+ te.getMessage());
			}
		}

		List<Message> message_list = new ArrayList<Message>();
		try {
			// lastCheck will be 0 if you've never checked for messages before.
			// This is here to prevent a situation where, the first time this code runs, 
			// it gets all the messages ever sent to the the authenticating Twitter account 
			// (myTweeter). Instead, the first time this runs, it will just update the last 
			// check (at the end of this function) so the next time it runs, it will have
			// a reasonable baseline to check for messages since
			if (lastMsgId > 0) {
				//System.err.println("lastMsgId is greater than 0");
				
				// TrafficTwitter.MSG_TYPE_DIRECT specifies to only check for direct messages (not @replies)....
				message_list = myTwitter.checkForMsgSinceId(lastMsgId,
						TrafficTwitter.MSG_TYPE_DIRECT);
				TLog.log(Level.FINER, String.format("Found %s new direct messages since id %s", message_list.size(), lastMsgId));
			} else {
				//System.err.println("lastMsgId is NOT greater than 0");
				if (lastCheck > 0) {
					//System.err.println("lastCheck is greater than 0");
					message_list = myTwitter.checkForMsgSince(lastCheck,
							TrafficTwitter.MSG_TYPE_DIRECT);
				} //else 
					//System.err.println("lastCheck is NOT greater than 0");
			}
		} catch (TwitterException e) {
			if (e.getClass() == TwitterRateLimitException.class) {
				// wait 15 minutes before trying again ....
				long chillaxUntil = System.currentTimeMillis() + 600000;
				TLog.log(Level.INFO, String.format("[%s] laying low until %s",
						this.sourceName, new Date(chillaxUntil)));
				setChillaxUntil(chillaxUntil);
				throw new TwitterDMSourceException(
						"Error occurred while checking for direct messages: "
								+ e.getMessage());
			}
			TLog.log(Level.WARNING,
					"Error occurred while checking for Direct Messages");
			throw new TwitterDMSourceException(e);
		}

		// Iterator over all direct messages
		Iterator<Message> msg_iterator = message_list.iterator();
		while (msg_iterator.hasNext()) {
			Message msg = msg_iterator.next();

			long msg_id = (long) msg.getId();
			TLog.log(Level.FINEST, String.format("Examining a direct message with id %s (created %s)", msg_id, msg.getCreatedAt()));
			
			// get the content of the direct message (should be a query
			// containing a street name)
			String msg_content = msg.getText();

			// get the username of the sender
			String msg_sender = msg.getSender().screenName;

			List<Status> status_list;
			try {
				if (msg_content.trim().isEmpty()) {
					// the message did not specify a query, so just get all the
					// statuses in the authenticating Twitter account's timeline
					status_list = myTwitter.getUserTimeline();
				} else {
					// search my statuses for the msg_content (presumably a
					// street name?)
					status_list = myTwitter.searchMyRecentStatuses(
							plusEncode(msg_content), 10800);
					
					// count this message as 'processed' only after you safely do a search for updates
					// when a list of messages are returned, the most recent is first, so you don't want
					// to overwrite the most recent id with a less recent one if you have multiple messages
					// to deal with here...
					lastMsgId = Math.max(lastMsgId,msg_id);
				}
			} catch (TwitterException te) {
				if (te.getClass() == TwitterRateLimitException.class) {
					// wait 15 minutes before trying again ....
					long chillaxUntil = System.currentTimeMillis() + 600000;
					TLog.log(Level.INFO, String.format(
							"[%s] laying low until %s", this.sourceName,
							new Date(chillaxUntil)));
					setChillaxUntil(chillaxUntil);
					throw new TwitterDMSourceException(
							"Error occurred while searching statuses: "
									+ te.getMessage());
				}
				TLog
						.log(Level.SEVERE,
								"Error talking to Twitter while responding to DM query");
				throw new TwitterDMSourceException(te);
			}

			// if the search didn't return any results
			if (status_list.size() == 0) {
				// create a new TrafficUpdate which tells user no search results
				// were found
				TrafficUpdate t = new TrafficUpdate(this, System
						.currentTimeMillis(),
						"No traffic updates within last 3 hrs. found for '"
								+ msg_content + "'");
				// add this special field to the TrafficUpdate's data to
				// represent the fact that it is destined for a particular
				// Twitter User
				// the TwitterDMSink will know what to do with this .....
				t.addData(TwitterSink.DM_RECIPIENT, msg_sender);
				updateList.add(t);

			} else {
				TLog.log(Level.FINER, "Got " + status_list.size()
						+ " results for query '" + msg_content + "'");
				// TLog.log(Level.FINEST, "[first one is]" +
				// ((Status)status_list.get(0)).text);

				// This is kind of a hack here, should really be handled on the
				// output side. To not annoy the
				// heck out of users, limit the number of DMs to be sent in
				// response to a query to 3.
				if (status_list.size() > 3)
					status_list = status_list.subList(0, 3);

				// the search did return some results!
				Iterator<Status> statusI = status_list.iterator();
				while (statusI.hasNext()) {
					Status s = statusI.next();

					String hour_minute;
					try {
						String date_string = s.getCreatedAt().toString();
						String hour_minute_second = (String) date_string
								.split(" ")[3];
						hour_minute = hour_minute_second.substring(0,
								hour_minute_second.lastIndexOf(":"));
					} catch (Exception e) {
						TLog.log(Level.WARNING,
								"Error parsing date string of status message.");
						TLog.log(Level.WARNING, e);
						hour_minute = s.getCreatedAt().toString();
					}
					// create a new TrafficUpdate for each result
					TrafficUpdate t = new TrafficUpdate(this, System
							.currentTimeMillis(), s.text + " [at "
							+ hour_minute + "]");

					// tag the update with the sender of the direct message
					t.addData(TwitterSink.DM_RECIPIENT, msg_sender);
					updateList.add(t);
				}
			}
		}

		// if you're supposed to update the time last checked ...
		if (updateLastCheck) {
			setLastCheckToNow();
		}

		return updateList;
	}

	/* Encodes specifically to work with Twitter's URI scheme */
	private String plusEncode(String val) {
		return val.replace(" ", "+");
	}

	/**
	 * A class which represents any exceptional conditions which arise during
	 * the processing of Twitter Direct messages containing queries.
	 * 
	 * @author Dan Greenblatt
	 * 
	 */
	public class TwitterDMSourceException extends TrafficSourceException {

		public TwitterDMSourceException() {
			super();
		}

		public TwitterDMSourceException(Exception e) {
			super(e);
		}

		public TwitterDMSourceException(String s) {
			super(s);
		}
	}
}
